{ "cells": [ { "cell_type": "markdown", "id": "61a24cbb", "metadata": {}, "source": [ "(adaptive-tutorial)=\n", "# Tutorial 4. Adaptive Measurements\n", "\n", "```{seealso}\n", "The complete source code of this tutorial can be found in\n", "\n", "{nb-download}`Tutorial 4. Adaptive Measurements.ipynb`\n", "```\n", "\n", "This tutorial requires familiarity with the **core concepts** of Quantify.\n", "We **highly recommended** to read the {ref}`user guide ` and follow {ref}`Tutorial 1. Controlling a basic experiment using MeasurementControl` and {ref}`Tutorial 2. Advanced capabilities of the MeasurementControl` first.\n", "\n", "In this tutorial, we explore the adaptive functionality of the {class}`.MeasurementControl`.\n", "With this mode, instead of predefining a grid of values to sweep through, we provide an optimization function and an initial state to the `meas_ctrl`.\n", "The `meas_ctrl` will then use this function to build the sweep. We import our usual modules and set up a `meas_ctrl` with visualization:" ] }, { "cell_type": "code", "execution_count": null, "id": "4874e435", "metadata": { "mystnb": { "code_prompt_show": "Imports and auxiliary utilities" }, "tags": [ "hide-cell" ] }, "outputs": [], "source": [ "import time\n", "\n", "import adaptive\n", "import numpy as np\n", "from qcodes import Instrument, ManualParameter\n", "from scipy import optimize\n", "\n", "import quantify_core.analysis.optimization_analysis as oa\n", "import quantify_core.visualization.pyqt_plotmon as pqm\n", "from quantify_core.analysis.interpolation_analysis import InterpolationAnalysis2D\n", "from quantify_core.data.handling import set_datadir, default_datadir\n", "from quantify_core.measurement.control import MeasurementControl\n", "from quantify_core.visualization.instrument_monitor import InstrumentMonitor" ] }, { "cell_type": "markdown", "id": "07057c0a", "metadata": {}, "source": [ "Before instantiating any instruments or starting a measurement we change the\n", "directory in which the experiments are saved using the\n", "{meth}`~quantify_core.data.handling.set_datadir`\n", "\\[{meth}`~quantify_core.data.handling.get_datadir`\\] functions.\n", "\n", "----------------------------------------------------------------------------------------\n", "\n", "⚠️ **Warning!**\n", "\n", "We recommend always setting the directory at the start of the python kernel and stick\n", "to a single common data directory for all notebooks/experiments within your\n", "measurement setup/PC.\n", "\n", "The cell below sets a default data directory (`~/quantify-data` on Linux/macOS or\n", "`$env:USERPROFILE\\\\quantify-data` on Windows) for tutorial purposes. Change it to your\n", "desired data directory. The utilities to find/search/extract data only work if\n", "all the experiment containers are located within the same directory.\n", "\n", "----------------------------------------------------------------------------------------" ] }, { "cell_type": "code", "execution_count": null, "id": "d6778cc3", "metadata": {}, "outputs": [], "source": [ "set_datadir(default_datadir())" ] }, { "cell_type": "code", "execution_count": null, "id": "c93ce3d0", "metadata": { "mystnb": { "remove-output": true } }, "outputs": [], "source": [ "meas_ctrl = MeasurementControl(\"meas_ctrl\")\n", "insmon = InstrumentMonitor(\"InstrumentMonitor\")\n", "plotmon = pqm.PlotMonitor_pyqt(\"plotmon_meas_ctrl\")\n", "meas_ctrl.instr_plotmon(plotmon.name)" ] }, { "cell_type": "markdown", "id": "57a00112", "metadata": {}, "source": [ "## Finding a minimum\n", "\n", "We will create a mock Instrument our `meas_ctrl` will interact with. In this case, it is a simple parabola centered at the origin." ] }, { "cell_type": "code", "execution_count": null, "id": "882fd32e", "metadata": {}, "outputs": [], "source": [ "para = Instrument(\"parabola\")\n", "\n", "para.add_parameter(\"x\", unit=\"m\", label=\"X\", parameter_class=ManualParameter)\n", "para.add_parameter(\"y\", unit=\"m\", label=\"Y\", parameter_class=ManualParameter)\n", "\n", "para.add_parameter(\n", " \"noise\", unit=\"V\", label=\"white noise amplitude\", parameter_class=ManualParameter\n", ")\n", "para.add_parameter(\n", " \"acq_delay\", initial_value=0.1, unit=\"s\", parameter_class=ManualParameter\n", ")\n", "\n", "\n", "def _amp_model():\n", " time.sleep(\n", " para.acq_delay()\n", " ) # for display purposes, just so we can watch the live plot update\n", " return para.x() ** 2 + para.y() ** 2 + para.noise() * np.random.rand(1)\n", "\n", "\n", "para.add_parameter(\"amp\", unit=\"V\", label=\"Amplitude\", get_cmd=_amp_model)" ] }, { "cell_type": "markdown", "id": "1999bc2a", "metadata": {}, "source": [ "Next, we will use the `optimize` package from `scipy` to provide our adaptive function.\n", "You can of course implement your own functions for this purpose, but for brevity we will use something standard and easily available.\n", "\n", "Then, we set our {ref}`Settables and Gettables` as usual, and define a new dictionary `af_pars`.\n", "The only required key in this object is `adaptive_function`, the value to be used by the adaptive function.\n", "The remaining fields in this dictionary are the arguments to the adaptive function itself. We also add some noise into the parabola to stress our adaptive function.\n", "\n", "**As such, it is highly recommended to thoroughly read the documentation around the adaptive function you are using.**\n", "\n", "We will use the `optimize.minimize` function (note this is passed by reference as opposed to calling the `minimize` function), which requires an initial state named `\"x0\"` and an algorithm to use named `\"method\"`.\n", "In this case, we are starting at `[-50, -50]` and hope to minimize these values relative to our parabola function.\n", "Of course, this parabola has its global minimum at the origin, thus these values will tend towards 0 as our algorithm progresses." ] }, { "cell_type": "code", "execution_count": null, "id": "ff1d9151", "metadata": { "tags": [ "hide-output" ] }, "outputs": [], "source": [ "meas_ctrl.settables([para.x, para.y])\n", "af_pars = {\n", " \"adaptive_function\": optimize.minimize, # used by meas_ctrl\n", " \"x0\": [-50, -50], # used by `optimize.minimize` (in this case)\n", " \"method\": \"Nelder-Mead\", # used by `optimize.minimize` (in this case)\n", " \"options\": {\"maxfev\": 100}, # limit the maximum evaluations of the gettable(s)\n", "}\n", "para.noise(0.5)\n", "meas_ctrl.gettables(para.amp)\n", "dset = meas_ctrl.run_adaptive(\"nelder_mead_optimization\", af_pars)" ] }, { "cell_type": "code", "execution_count": null, "id": "1ad6c852", "metadata": {}, "outputs": [], "source": [ "dset" ] }, { "cell_type": "code", "execution_count": null, "id": "8733dbaa", "metadata": {}, "outputs": [], "source": [ "plotmon.main_QtPlot" ] }, { "cell_type": "code", "execution_count": null, "id": "f082765c", "metadata": {}, "outputs": [], "source": [ "plotmon.secondary_QtPlot" ] }, { "cell_type": "markdown", "id": "dacd3b6c", "metadata": {}, "source": [ "We can see from the graphs that the values of the settables in the dataset snake towards 0 as expected. Success!\n", "\n", "### Analysis\n", "\n", "There are several analysis classes available in `quantify-core` that can be used to visualize and extract relevant information from the results of these adaptive measurements.\n", "\n", "The {class}`~quantify_core.analysis.optimization_analysis.OptimizationAnalysis` class searches the dataset for the optimal datapoint and provides a number of useful plots to visualize the convergence of the measurement result around the minimum." ] }, { "cell_type": "code", "execution_count": null, "id": "35a9c0d8", "metadata": {}, "outputs": [], "source": [ "a_obj = oa.OptimizationAnalysis(dset)\n", "a_obj.run()\n", "a_obj.display_figs_mpl()" ] }, { "cell_type": "markdown", "id": "7aaf2250", "metadata": {}, "source": [ "The analysis generates plots of each of the variables versus the number of iteration steps completed. The figures show the data converging on the optimal value.\n", "\n", "The {class}`~quantify_core.analysis.interpolation_analysis.InterpolationAnalysis2D` class can be used to generate a 2-dimensional heatmap that interpolates between a set of irregularly spaced datapoints." ] }, { "cell_type": "code", "execution_count": null, "id": "8f010339", "metadata": {}, "outputs": [], "source": [ "a_obj = InterpolationAnalysis2D(dset)\n", "a_obj.run()\n", "a_obj.display_figs_mpl()" ] }, { "cell_type": "markdown", "id": "6be8ebb3", "metadata": {}, "source": [ "## Adaptive Sampling\n", "\n", "Quantify is designed to be modular and the adaptive functions support is no different. To this end, the `meas_ctrl` has first-class support for the `adaptive` package.\n", "Let's see what the same experiment looks like with this module. Note the fields of the `af_pars` dictionary have changed to be compatible with the different adaptive functions that we are using.\n", "\n", "As a practical example, let's revisit a Resonator Spectroscopy experiment. This time we only know our device has a resonance in 6-7 GHz range.\n", "We really don't want to sweep through a million points, so instead let's use an adaptive sampler to quickly locate our peak." ] }, { "cell_type": "code", "execution_count": null, "id": "075258bc", "metadata": {}, "outputs": [], "source": [ "res = Instrument(\"Resonator\")\n", "\n", "res.add_parameter(\"freq\", unit=\"Hz\", label=\"Frequency\", parameter_class=ManualParameter)\n", "res.add_parameter(\"amp\", unit=\"V\", label=\"Amplitude\", parameter_class=ManualParameter)\n", "_fwhm = 15e6 # pretend you don't know what this value is\n", "_res_freq = 6.78e9 # pretend you don't know what this value is\n", "_noise_level = 0.1\n", "\n", "\n", "def lorenz():\n", " \"\"\"A Lorenz model function.\"\"\"\n", " time.sleep(0.02) # for display purposes, just so we can watch the graph update\n", " return (\n", " 1\n", " - (\n", " res.amp()\n", " * ((_fwhm / 2.0) ** 2)\n", " / ((res.freq() - _res_freq) ** 2 + (_fwhm / 2.0) ** 2)\n", " )\n", " + _noise_level * np.random.rand(1)\n", " )\n", "\n", "\n", "res.add_parameter(\"S21\", unit=\"V\", label=\"Transmission amp. S21\", get_cmd=lorenz)" ] }, { "cell_type": "code", "execution_count": null, "id": "312542e3", "metadata": { "tags": [ "hide-output" ] }, "outputs": [], "source": [ "_noise_level = 0.0\n", "res.amp(1)\n", "meas_ctrl.settables([res.freq])\n", "af_pars = {\n", " \"adaptive_function\": adaptive.learner.Learner1D,\n", " \"goal\": lambda l: l.npoints > 99,\n", " \"bounds\": (6.0e9, 7.0e9),\n", "}\n", "meas_ctrl.gettables(res.S21)\n", "dset = meas_ctrl.run_adaptive(\"adaptive sample\", af_pars)" ] }, { "cell_type": "code", "execution_count": null, "id": "41581199", "metadata": {}, "outputs": [], "source": [ "dset" ] }, { "cell_type": "code", "execution_count": null, "id": "8f2e3381", "metadata": {}, "outputs": [], "source": [ "plotmon.main_QtPlot" ] }, { "cell_type": "markdown", "id": "e0795fee", "metadata": {}, "source": [ "## FAQ\n", "\n", "### Can I return multi-dimensional data from a Gettable in Adaptive Mode?\n", "\n", "Yes, but only the first dimension (`y0`) will be considered by the adaptive function;\n", "the remaining dimensions will merely be saved to the dataset." ] } ], "metadata": { "file_format": "mystnb", "jupytext": { "text_representation": { "extension": ".md", "format_name": "myst" } }, "kernelspec": { "display_name": "python3", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 5 }